package engine.sound;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

import org.lwjgl.BufferUtils;
import org.lwjgl.openal.AL;
import org.lwjgl.openal.AL10;
import org.lwjgl.openal.ALC10;
import org.lwjgl.openal.ALCcontext;
import org.lwjgl.openal.ALCdevice;
import org.lwjgl.openal.OpenALException;

import engine.util.ogg.OggInputStream;

public class SoundManager {
	/** Cancel loading sound if sound doesn't work*/
	private static boolean soundworks = false;

	/** List of loaded files */
	private static HashMap<String, Integer> loadedFiles = new HashMap<String, Integer>();

	/** List of sources */
	private static ArrayList<Integer> sources = new ArrayList<Integer>();

	/** Position of the listener. */
	private static FloatBuffer listenerPosition = BufferUtils.createFloatBuffer(3).put(new float[] { 0.0f, 0.0f, 0.0f });

	/** Velocity of the listener. */
	private static FloatBuffer listenerVelocity = BufferUtils.createFloatBuffer(3).put(new float[] { 0.0f, 0.0f, 0.0f });

	/** Orientation of the listener. (first 3 elements are "at", second 3 are "up") */
	private static FloatBuffer listenerOrientation = BufferUtils.createFloatBuffer(6).put(new float[] { 0.0f, 0.0f, -1.0f,  0.0f, 1.0f, 0.0f });
	
	
	private SoundManager() {}

	public static void init() {
		initAL();
		
		initListenerValues();
	}
	
	private static void initAL() {
		try {
			AL.create();
			soundworks = true;
		} catch(Exception e) {
			e.printStackTrace();
		
			return;
		}
		
		AL10.alGetError();
	}
	
	/**
	 * void setListenerValues()
	 * 
	 * We already defined certain values for the Listener, but we need to tell
	 * OpenAL to use that data. This function does just that.
	 */
	private static void initListenerValues() {
		listenerOrientation.flip();
		listenerPosition.flip();
		listenerVelocity.flip();
		AL10.alListener(AL10.AL_POSITION, listenerPosition);
		AL10.alListener(AL10.AL_VELOCITY, listenerVelocity);
		AL10.alListener(AL10.AL_ORIENTATION, listenerOrientation);
	}
	
	public static void setListenerPosition(float[] position) {
		FloatBuffer listenerPos = BufferUtils.createFloatBuffer(3).put(position);
		listenerPos.flip();
		
		SoundManager.listenerPosition = listenerPos;
		
		AL10.alListener(AL10.AL_POSITION, listenerPos);
	}
	
	public static float[] getListenerPosition() {
		return new float[]{listenerPosition.get(0), listenerPosition.get(1), listenerPosition.get(2)};
	}
	
	public static void setListenerVelocity(float[] listenerVelocity) {
		FloatBuffer listenerVel = BufferUtils.createFloatBuffer(6).put(listenerVelocity);
		listenerVel.flip();
		
		SoundManager.listenerVelocity = listenerVel;
		
		AL10.alListener(AL10.AL_VELOCITY, listenerVel);
	}
	
	public static float[] getListenerVelocity() {
		return new float[]{listenerVelocity.get(0), listenerVelocity.get(1), listenerVelocity.get(2)};
	}

	public static void setListenerOrientation(float[] orientation) {
		FloatBuffer listenerOri = BufferUtils.createFloatBuffer(6).put(orientation);
		listenerOri.flip();
		
		SoundManager.listenerOrientation = listenerOri;
		
		AL10.alListener(AL10.AL_ORIENTATION, listenerOri);
	}
	
	public static float[] getListenerOrientation() {
		return new float[]{listenerOrientation.get(0), listenerOrientation.get(1), listenerOrientation.get(2), 
				listenerOrientation.get(3), listenerOrientation.get(4), listenerOrientation.get(5)};
	}
	
	/**
	 * 1) Creates a source. 2) Calls 'GetLoadedALBuffer' with 'path' and uses
	 * the returned buffer id as it's sources buffer. 3) Returns the source id.
	 */
	public static int loadALSample(String fileName) {
		IntBuffer source = BufferUtils.createIntBuffer(1);
		int buffer;
		int result;

		// Get the files buffer id (load it if necessary).
		buffer = getLoadedALBuffer(fileName);

		// Generate a source.
		AL10.alGenSources(source);

		if((result = AL10.alGetError()) != AL10.AL_NO_ERROR)
			throw new OpenALException(getALErrorString(result));
		
		// Setup the source properties.
		AL10.alSourcei(source.get(0), AL10.AL_BUFFER, buffer);

		// Save the source id.
		sources.add(new Integer(source.get(0)));

		// Return the source id.
		return source.get(0);
	}

	/**
	 * 1) Checks if file has already been loaded. 2) If it has been loaded
	 * already, return the buffer id. 3) If it has not been loaded, load it and
	 * return buffer id.
	 */
	private static int getLoadedALBuffer(String fileName) {
		if(!loadedFiles.containsKey(fileName)) {
			loadedFiles.put(fileName, loadALBuffer(fileName));
		}
		
		return loadedFiles.get(fileName);
	}
	
	/**
	 * 1) Creates a buffer. 2) Loads a ogg file into the buffer. 3) Returns the
	 * buffer id.
	 * 
	 * @param fileName
	 *            The reference to the OGG file in the classpath
	 * @return The Sound read from the OGG file
	 * @throws IOException
	 *             Indicates a failure to load the OGG
	 */
	private static int loadALBuffer(String fileName) {
		InputStream in = null;
		File file = new File(fileName);
		
		if(file.exists()) {
			try {
				in = new FileInputStream(fileName);
			} catch (FileNotFoundException e1) {
				System.out.println("File not found as input stream: " + fileName);
			}
		} else {
			in = SoundManager.class.getClassLoader().getResourceAsStream(fileName);
		}

		if(in == null) {
			throw new RuntimeException("Resource not found: " + fileName);
		}

		if(!soundworks) {
			System.out.println("sound does not work");

			return -1;
		}
		
		int result;		
		
		IntBuffer buffer = BufferUtils.createIntBuffer(1);

		// Load wav data into a buffers.
		AL10.alGenBuffers(buffer);

		if((result = AL10.alGetError()) != AL10.AL_NO_ERROR) {
			throw new OpenALException(getALErrorString(result));
		}	
			
		try {
			ByteArrayOutputStream dataout = new ByteArrayOutputStream();

			OggInputStream oggInput = new OggInputStream(in);

			while (!oggInput.atEnd()) {
				dataout.write(oggInput.read());
			}

			byte[] data = dataout.toByteArray();
			ByteBuffer oggData = ByteBuffer.allocateDirect(data.length);
			oggData.put(data);
			oggData.rewind();

			AL10.alBufferData(buffer.get(0), 
					oggInput.getChannels() > 1 ? AL10.AL_FORMAT_STEREO16 : AL10.AL_FORMAT_MONO16, 
					oggData, oggInput.getRate());
			
			oggInput.close();
		} catch(Exception e) {
			System.out.println("Unable to load: " + fileName);
		}

		// Do another error check and return.
		if((result = AL10.alGetError()) != AL10.AL_NO_ERROR) {
			throw new OpenALException(getALErrorString(result));
		}

		return buffer.get(0);
	}
	
	/**
	 * 1) Identify the error code. 2) Return the error as a string.
	 */
	private static String getALErrorString(int err) {
		switch(err) {
		case AL10.AL_NO_ERROR:
			return "AL_NO_ERROR";
		case AL10.AL_INVALID_NAME:
			return "AL_INVALID_NAME";
		case AL10.AL_INVALID_ENUM:
			return "AL_INVALID_ENUM";
		case AL10.AL_INVALID_VALUE:
			return "AL_INVALID_VALUE";
		case AL10.AL_INVALID_OPERATION:
			return "AL_INVALID_OPERATION";
		case AL10.AL_OUT_OF_MEMORY:
			return "AL_OUT_OF_MEMORY";
		default:
			return "No such error code";
		}
	}

	/**
	 * 1) Releases all buffers. 2) Releases all sources.
	 */
	public static void destroy() {
		IntBuffer scratch = BufferUtils.createIntBuffer(1);

		// Release all source data.
		for(Iterator<Integer> iter = sources.iterator(); iter.hasNext();) {
			scratch.put(0, ((Integer) iter.next()).intValue());
			do {
				AL10.alSourceStop(scratch);
			} while(AL10.alGetSourcei(scratch.get(0), AL10.AL_SOURCE_STATE) == AL10.AL_PLAYING);
			
			AL10.alDeleteSources(scratch);
		}
		
		// Release all buffer data.
		for(Integer i: loadedFiles.values()) {
			scratch.put(0, i); 
			AL10.alDeleteBuffers(scratch);
		}

		// Destroy the lists.
		sources.clear();
		loadedFiles.clear();
		
		ALCcontext context;
		ALCdevice device;
		
		context = ALC10.alcGetCurrentContext();
		 
		//Get device for active context
		device = ALC10.alcGetContextsDevice(context);
		 
		//Disable context
		ALC10.alcMakeContextCurrent(null);
		 
		//Release context(s)
		ALC10.alcDestroyContext(context);
		 
		//Close device
		ALC10.alcCloseDevice(device);
	}
}
